feat!: multi-GNSS PVT with GPS L2C/L5/L1C and Galileo E5a#48
Merged
Conversation
Codecov Report❌ Patch coverage is
Additional details and impacted files@@ Coverage Diff @@
## master #48 +/- ##
==========================================
+ Coverage 92.77% 98.27% +5.49%
==========================================
Files 7 7
Lines 360 464 +104
==========================================
+ Hits 334 456 +122
+ Misses 26 8 -18 ☔ View full report in Codecov by Harness. 🚀 New features to boost your workflow:
|
Contributor
Benchmark Results (Julia v1)Time benchmarks
Memory benchmarks
|
giove-a
force-pushed
the
sc/multi-gnss
branch
2 times, most recently
from
July 5, 2026 16:22
f4563a3 to
6e2291a
Compare
Combine satellites from multiple GNSS in `calc_pvt` and add the modern
GPS and Galileo civil signals.
Multi-GNSS:
- Estimate one receiver clock bias per GNSS time system; the state vector
becomes [x, y, z, tc_1, …, tc_M] and a fix needs n >= 3 + M satellites.
Time-system grouping uses GNSSSignals.get_time_system(signal) and the
GPST()/GST() TimeSystem singletons.
- GGTO fallback: when the independent inter-system-bias solve is
under-determined but GPS is present and a Galileo satellite carries the
Galileo-GPS Time Offset, collapse the Galileo clock onto GPS via the
broadcast offset (range_offset = -c * GGTO), enabling a 4-satellite
GPS+Galileo fix.
- `PVTSolution` gains `reference_system` and `inter_system_biases`, keyed by
GNSSSignals.TimeSystem.
Inter-frequency bias:
- When satellites are processed on more than one frequency band, estimate
one receiver inter-frequency bias (IFB) per band beyond a reference band —
the differential RF-chain hardware delay — shared across constellations on
that band (GPS L1/L1C/Galileo E1 -> :L1; GPS L5/Galileo E5a -> :L5;
GPS L2C -> :L2; see `GNSSSignals.get_band_id`). The state vector extends to
[x, y, z, tc_1…tc_M, ifb_1…ifb_B] and a fix needs n >= 3 + M + B satellites.
- `PVTSolution` gains `inter_frequency_biases` (relative to the reference
band; empty for single-band fixes). Velocity is unaffected (a constant
bias, no drift term); the IFB composes with the GGTO clock collapse.
- Observability: the per-system clock and per-band IFB columns form the
incidence matrix of the (constellation × band) coverage graph. When that
graph is disconnected — constellations occupying disjoint bands — those
columns are collinear and the independent layout is rank-deficient, which
previously yielded degenerate clock/IFB estimates. The layout now detects
this and falls back: collapse the Galileo clock onto GPS via the GGTO when
available, otherwise estimate the inter-system bias and IFB as a single
common parameter (they can no longer be separated).
New signals:
- GPS L5 (CNAV) and L1C (CNAV-2): quasi-Keplerian ephemerides
(A_REF + ΔA, Ω̇_REF + ΔΩ̇, rate terms) via `orbital_elements`, full-week
number, ITOW/TOI time-of-week reconstruction for L1C-D.
- GPS L2C (CNAV on the new L2 band): shares the CNAV `GPSCNAVData` container
with L5-I — the signal is fixed by the decoder's constants type, not the
data type — so it reuses the CNAV orbit/clock/week/Klobuchar path; it ranges
on the L2 CM data component with the ISC_L2C inter-signal correction, and
introduces the L2 band into the inter-frequency-bias layout.
- Galileo E5a (F/NAV): shares the Galileo ephemeris/clock/GGTO/NTCM-G layout
with E1B; applies the E1-E5a broadcast group delay.
- Galileo E1C, GPS L1C-P, GPS L5-Q and GPS L2 CL pilots are not separate
measurements: with joint same-band tracking the band yields one pseudorange,
so pilots aid tracking upstream only.
Dispatch is grouped with `GalileoNavData`, `GPSNavData` and `GPSModernNavData`
type aliases; Klobuchar is extended to all GPS civil signals and NTCM-G to
both Galileo messages.
Group delay / inter-signal correction is selected by the *ranging* signal
(`SatelliteState.system`) with values read from the decoder's message, so a
band's pseudorange may be generated on a pilot (e.g. GPS L1C-P) while the
ephemeris/clock come from the data-component decoder (CNAV-2 → ISC_L1CP vs
ISC_L1CD vs ISC_L1CA; CNAV → ISC_L5I5/ISC_L5Q5 vs the single ISC_L2C
shared by CM and CL). The transmit-time bit-count
term takes its symbol rate from GNSSSignals.get_data_frequency on the decoder
state (GNSSDecoder 3.6), not the tracked signal, so a pilot's 0 Hz data rate is
never divided by, and CNAV picks the right rate per signal (L5-I 100 Hz vs
L2C-M 50 Hz) via the decoder's constants type. Galileo's broadcast group delay
stays per band (decoder-determined).
Solver and outputs:
- `sats` is keyed by (signal, PRN) in a `Dictionary` so the same PRN can
appear across constellations and bands; new `get_sat_info` accessor.
- `SatInfo` carries the post-fit pseudorange residual.
- DOP is rotated into the local ENU frame for HDOP/VDOP and uses a
non-throwing Cholesky; TDOP reports the reference system's clock.
- Velocity/clock-drift solved via allocation-free 4x4 normal equations with
a single common receiver clock drift across all systems.
- calc_pvt skips the atmospheric-delay prediction (a per-satellite geodetic +
ENU pass) when neither ionospheric nor tropospheric correction is active, and
builds the satellite-position matrix with `stack` in a single pass.
Type-level getters and time scale (GNSSSignals 3.3):
- The ionospheric/velocity carrier-frequency reads take constants from the
signal *type*, never a constructed instance:
constructing a signal builds its full code table (~120 KB GPS L1CA, ~620 KB
Galileo E1B) and this ran once per satellite, so it cut calc_pvt allocations
~97% (GPS) / ~99% (Galileo).
- The time-scale origin comes from GNSSSignals.get_system_start_time (UTC),
converted to a TAIEpoch via AstroTime's leap-aware from_utc
(system_start_epoch); the hardcoded epochs and leap-second literals are gone.
system_start_epoch lands on an integer TAI second for GPS/Galileo, so the
reported-time build assumes start_time.fraction == 0 (noted inline).
- Per-signal and per-band classification uses GNSSSignals.get_signal_id
(:GPSL1CA, …) and GNSSSignals.get_band_id (:L1, …) on each satellite's
signal, instead of package-local helpers (GNSSSignals 3.3).
- The nav-message symbol rate for the transmit-time bit count comes from
GNSSSignals.get_data_frequency on the decoder state (GNSSDecoder 3.6),
replacing the six package-local nav_data_frequency methods.
Compat: GNSSDecoder 3.6, GNSSSignals 3.3, add Dictionaries, drop Statistics.
Tests: add cnav.jl, dop.jl, galileo_e5a.jl, gps_l2c.jl and
inter_frequency_bias.jl; the satellite fixtures move to test/fixtures.jl,
shared with benchmark/fixtures.jl (which keeps a GNSSDecoder major-version
shim so AirspeedVelocity can run the script against the base revision); cover combined GPS+Galileo fixes, the GGTO fallback,
(signal, PRN) keying, per-satellite residuals, ranging-signal-selected group
delay (incl. pilot-referenced ranges and ISC_L2C), per-band inter-frequency-bias
estimation (incl. GPS L1+L2), and the disjoint-band fallback.
BREAKING CHANGE: `PVTSolution.sats` is now a
`Dictionary{Tuple{Symbol,Int},SatInfo}` (was `Dict{Int,SatInfo}`); index it
with `get_sat_info(pvt, signal, prn)`. `SatInfo` gains a `residual` field and
`calc_DOP` takes the user position and primary clock index. The
`get_gdop`/`get_pdop`/`get_hdop`/`get_vdop`/`get_tdop` accessors are removed;
read DOP from the `dop` field instead (e.g. `pvt.dop.GDOP`).
`get_num_used_sats` is removed: with `sats` keyed by (signal, PRN),
`length(pvt.sats)` counts measurements, not satellites.
`get_frequency_offset` is removed: compute it inline as
`pvt.relative_clock_drift * base_frequency`.
`PVTSolution.reference_system` and the `inter_system_biases` keys are now
`GNSSSignals.TimeSystem` values (GPST()/GST()), not :GPS/:Galileo symbols.
Requires GNSSSignals 3.3 and GNSSDecoder 3.6.
Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Summary
Combine satellites from multiple GNSS in
calc_pvtand add the modern GPS and Galileo civil signals (GPS L2C, L5, L1C; Galileo E5a). Breaking:PVTSolutiongrows new fields and the solver's minimum-satellite requirement changes.Requires GNSSDecoder ≥ 3.3.1 and GNSSSignals ≥ 2.6.
Multi-GNSS
[x, y, z, tc_1, …, tc_M]and a fix needsn ≥ 3 + Msatellites.range_offset = −c · GGTO), enabling a 4-satellite GPS+Galileo fix.PVTSolutiongainsreference_systemandinter_system_biases.Inter-frequency bias (IFB)
:L1; GPS L5/Galileo E5a →:L5; GPS L2C →:L2). State extends to[x, y, z, tc_1…tc_M, ifb_1…ifb_B]; a fix needsn ≥ 3 + M + B.PVTSolutiongainsinter_frequency_biases(relative to the reference band; empty for single-band fixes). Velocity is unaffected (constant bias, no drift term); the IFB composes with the GGTO clock collapse.New signals
A_REF + ΔA,Ω̇_REF + ΔΩ̇, rate terms), full-week number, ITOW/TOI time-of-week reconstruction for L1C-D.GPSCNAVDatacontainer with L5-I — the signal is fixed by the decoder's constants type, not the data type — reusing the CNAV orbit/clock/week/Klobuchar path; ranges on the L2 CM data component withISC_L2C.Group-delay / inter-signal correction
Selected by the ranging signal (
SatelliteState.system) with values read from the decoder's message, so a band's pseudorange may be generated on a pilot (e.g. GPS L1C-P) while the ephemeris/clock come from the data-component decoder (CNAV-2 →ISC_L1CP/ISC_L1CD/ISC_L1CA; CNAV →ISC_L5I5/ISC_L2C).These terms are ~metre-level corrections a satellite may not have broadcast yet when its ephemeris and clock are already complete, so any of them can be
nothingon a usable decoder.correct_by_group_delaytherefore coalesces a missing term to zero (group_delay_term): apply each when present, treat a missing one as zero, never throw. Terms that the decoder's positioning-readiness gate guarantees (LNAVT_GD, Galileo BGD, the CNAV-2 subframe-2 terms) are always present; the ones that can genuinely lag a fix (CNAVT_GD/ISC_L2C/ISC_L5I5, CNAV-2ISC_L1CA) are the ones this guards.Tests
New:
test/cnav.jl,test/gps_l2c.jl,test/galileo_e5a.jl,test/dop.jl,test/inter_frequency_bias.jl; expandedtest/pvt.jl. Full suite green against GNSSDecoder 3.3.1.🤖 Generated with Claude Code